package org.invigorare.auctions.comm;

import java.util.*;
import java.util.Map.Entry;

import org.invigorare.auctions.R;
import org.invigorare.auctions.comm.EntrySyncResult.Status;
import org.invigorare.auctions.comm.InboundSyncResult.InboundStatus;
import org.invigorare.auctions.data.AppSettings;
import org.invigorare.auctions.data.EntryDataOpener;
import org.invigorare.auctions.data.LicenseDataOpener;
import org.invigorare.auctions.data.ProduceDataOpener;
import org.invigorare.auctions.data.SaleDataOpener;
import org.invigorare.auctions.data.SynchronizableInboundDataOpener;
import org.invigorare.auctions.data.entities.AuctionSale;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.util.Log;

public class DbSynchronizer {

	Activity currActivity;
	Context context;
	WebServiceLayer web;

	private static final String INVALID_EMPLOYEE_ID = "\"invalid employee id\"";
	
	private static final Object lock = new Object();
	
	public DbSynchronizer(Activity currentActivity) {
		this.currActivity = currentActivity;
		this.context = currentActivity.getApplicationContext();
		this.web = new WebServiceLayer(context);
	}
	
	@Deprecated
	private long getLastUpdateTime() {
		long lastUpdateTime;

		SynchronizableInboundDataOpener produce = new ProduceDataOpener(context);
		long produceTime = produce.getOldestUpdateTime();
		produce.close();
		
		SynchronizableInboundDataOpener licenses = new LicenseDataOpener(context);
		long licenseTime = licenses.getOldestUpdateTime();
		licenses.close();
		
		lastUpdateTime = Math.min(produceTime, licenseTime);
		Log.i("DBSYNC", "Last updated time min(" + produceTime + ", " + licenseTime + ") : " + lastUpdateTime);
		// TODO: look at all DB's
		return lastUpdateTime;
	}
	
	/** 
	 * Attempts inbound synchronization and returns the results as a human-readable announcement string.
	 * @return
	 */
	public synchronized InboundSyncResult syncDbInbound() {
		synchronized(lock) {
			
			AppSettings settings = new AppSettings(context);
			String userId = settings.getEmployeeId();
			long lastUpdateTime = settings.getLastSyncTime();
			
			try {
				/* Fetch Web Data */
				String jsonResponse = web.fetchInboundUpdate(currActivity, lastUpdateTime, userId);

				/* Parse Web Data */
				JSONParser jsonParser = new JSONParser();
				Log.i("DB SYNC", "Inbound update data: \n" + jsonResponse);
				
				if (jsonResponse.equalsIgnoreCase(INVALID_EMPLOYEE_ID)) {
					return new InboundSyncResult(InboundStatus.INVALID_EMPLOYEE_ID, null, false);
				} else if (jsonResponse.equals(WebServiceLayer.ERROR_UNRESOLVED_HOST)) {
					return new InboundSyncResult(InboundStatus.UNRESOLVED_HOST, null, false);
				} else if (jsonResponse.equals(WebServiceLayer.ERROR_NODATA)) {
					return new InboundSyncResult(InboundStatus.WEBSERVICE_OTHER_ERROR, null, false);
				}
				
				try {
					JSONObject jsonMap = (JSONObject) jsonParser.parse(jsonResponse);
					
					/* Sync database tables: ADD NEW TABLES HERE as needed */
					DbSyncResult produceUpdateReport = syncOneInboundDb(TABLENAME_PRODUCE, new ProduceDataOpener(context), jsonMap, getColMap_Produce());
					DbSyncResult licenseUpdateReport = syncOneInboundDb(TABLENAME_LICENSE, new LicenseDataOpener(context), jsonMap, getColMap_License());
					DbSyncResult entryUpdateReport   = syncOneInboundDb(TABLENAME_GATE_ENTRY, new EntryDataOpener(context), jsonMap, getColMap_Entry());
					
					/* Sync settings */
					boolean settingsSyncSuccess = syncSettings(new AppSettings(context), jsonMap, getNameMap_Fees());
					
					return new InboundSyncResult(InboundStatus.NORMAL, null, settingsSyncSuccess, 
							produceUpdateReport,
							licenseUpdateReport,
							entryUpdateReport//,
							//syncSettingsReport
					);
					
				} catch (IllegalStateException e) {
					e.printStackTrace();
					return new InboundSyncResult(InboundStatus.JSON_PARSE_ERROR, e, false);
				} catch (ParseException e) {
					e.printStackTrace();
					return new InboundSyncResult(InboundStatus.JSON_PARSE_ERROR, e, false);
				}
				
			} catch (NoConnectionException e) {
				e.printStackTrace();
				return new InboundSyncResult(InboundStatus.NO_CONNECTIONS, e, false);
			}
		}
	}
	
	/** 
	 * Attempts outbound synchronization and returns the results as a human-readable announcement string.
	 * @return
	 */
	public synchronized DbSyncResult syncDbOutbound() {
		synchronized(lock) {
			
			SaleDataOpener saleDb = new SaleDataOpener(context);
			List<Integer> unsyncedRowIds = saleDb.getUnsyncedRowIds();
			List<Map<String, Object>> unsynced = saleDb.getUnsyncedRows(AuctionSale.DB_KEYS());
			
			List<AuctionSale> sales = new ArrayList<AuctionSale>(unsynced.size());
			for (Map<String, Object> entryMap : unsynced) {
				sales.add(new AuctionSale(entryMap));
			}
			
			Log.i("DBSYNC", "trying to outbound update " + sales.size() + " entries");
			
			DbSyncResult results = web.doOutboundSaleUpdates(currActivity, sales).associateRowIds(unsyncedRowIds);
			List<Integer> successfulRows = results.getRowsIdsWithStatus(Status.SUCCESS);
			saleDb.markRowsSent(successfulRows);
			
			return results;
		}
	}
	
	private DbSyncResult syncOneInboundDb(String jsonTableName, SynchronizableInboundDataOpener dbOpener, JSONObject jsonMap, Map<String, String> translatedColumns) {
		
		/* Extract the update time */
		long reportedServerTime = ((Number) jsonMap.get(META_SERVERTIME)).longValue();
		
		/* Process the updated table entries */
		JSONArray entryList = (JSONArray) jsonMap.get(jsonTableName);
		List<ContentValues> outputList = new ArrayList<ContentValues>(entryList.size());
		
		for (JSONObject entry : (List<JSONObject>) entryList) {
			
			/* Map the JSON column names into DB column names */
			ContentValues cv = new ContentValues(translatedColumns.size());
			Set<Entry<String, String>> colNames = translatedColumns.entrySet();
			
			for (Entry<String, String> col : colNames) {
				String jsonKey = col.getKey();
				String androidKey = col.getValue();
				
				String value = (String) entry.get(jsonKey);
				cv.put(androidKey, value);
			}
			
			outputList.add(cv);
		}
		
		/* Add all the mapped entries */
		dbOpener.addNewRows(outputList, reportedServerTime);

		List<EntrySyncResult> statuses = new ArrayList<EntrySyncResult>(outputList.size());
		for (int i = 0; i < outputList.size(); i++) statuses.add(new EntrySyncResult(Status.SUCCESS, null)); // TODO: this is clunky... having to add dummy STATUS items
		return new DbSyncResult(jsonTableName, null, statuses);
	}
	
	/**
	 * 
	 * @param settings
	 * @param jsonMap
	 * @param feeNameMap
	 * @return whether the settings sync was successful
	 */
	private boolean syncSettings(AppSettings settings, JSONObject jsonMap, Map<String, String> feeNameMap) {
		
		/* Extract the update time */
		long reportedServerTime = ((Number) jsonMap.get(META_SERVERTIME)).longValue();

		/* Extract settings */
		JSONObject settingsMap = (JSONObject) jsonMap.get(META_SETTINGS);
		String mandiName = (String) settingsMap.get(SETTINGS_MANDI_NAME);
		String employeeName = (String) settingsMap.get(SETTINGS_EMPLOYEE_NAME);
		
		/* Translate the fee table from JSON keys to SharedPrefs keys */
		Map<String, Float> feeMap = new HashMap<String, Float>(feeNameMap.size()); // in AppSettings keynamespace
		for (Map.Entry<String,String> entry : feeNameMap.entrySet()) {
			String jsonKey = entry.getKey();
			String appSettsKey = entry.getValue();
			float thisFee = ((Number)settingsMap.get(jsonKey)).floatValue();
			feeMap.put(appSettsKey, thisFee);
		}
		
		boolean success = settings.updateAppSettings(mandiName, employeeName, feeMap, reportedServerTime);
		return success;
	}
	
	/* JSON Field names, as returned by the web service */
	protected static final String TABLENAME_PRODUCE = "products";
	protected static final String TABLENAME_LICENSE = "licenses";
	protected static final String TABLENAME_GATE_ENTRY = "entries";
	protected static final String META_SERVERTIME = "updated";
	protected static final String META_SETTINGS = "settings";
	
	/* JSON: Produce */
	protected static final String PRODUCE_WEB_ID = "id";
	protected static final String PRODUCE_NAME_EN = "name_en";
	protected static final String PRODUCE_NAME_HI = "name_hi";
	
	/* JSON: License */
	protected static final String LICENSE_NAME = "l.name";
	protected static final String LICENSE_ADDR_STREET = "l.addr_street";
	protected static final String LICENSE_ADDR_STATE = "l.addr_state";
	protected static final String LICENSE_ADDR_PINCODE = "l.addr_pincode";
	protected static final String LICENSE_SELLER_NO = "l.license_seller";
	protected static final String LICENSE_BUYER_NO = "l.license_buyer";
	protected static final String LICENSE_ORG = "l.organization_name";
	
	/* JSON: Entry */
	protected static final String ENTRY_ID = "id";
	protected static final String ENTRY_TIMESTAMP = "timestamp";
	protected static final String ENTRY_DRIVER_NAME = "driver_name";
	protected static final String ENTRY_VEHICLE_NO = "vehicle_number";
	protected static final String ENTRY_VEHICLE_TYPE = "vehicle_type";
	protected static final String ENTRY_PROD_ID = "product_id";
	protected static final String ENTRY_PROD_WEIGHT = "product_weight";
	protected static final String ENTRY_PROD_FARM = "product_farm";
	protected static final String ENTRY_BEARER_LIC = "product_farm";
	protected static final String ENTRY_AGENT_LIC = "product_farm";
	//protected static final String ENTRY_EMP_ID, FEES;
	
	/* Sale table mapping done in AuctionSale entity */
	
	/* JSON: Settings */
	protected static final String SETTINGS_MANDI_NAME = "name";
	protected static final String SETTINGS_EMPLOYEE_NAME = "employee_name";
	
	protected static final String FEE_UNLOAD = "fee_unload";
	protected static final String FEE_CLEAN = "fee_clean";
	protected static final String FEE_WEIGH = "fee_weigh";
	protected static final String FEE_PACK = "fee_pack";
	protected static final String FEE_STITCH = "fee_stitch";
	protected static final String FEE_LOAD = "fee_load";
	protected static final String COMMISSION_AGENT = "commission_agent";
	protected static final String COMMISSION_MANDI = "commission_mandi";
	protected static final String[] FEES_COMMISSION_FIELDS = new String[] {
		FEE_UNLOAD, FEE_CLEAN, FEE_WEIGH, FEE_PACK, FEE_STITCH, FEE_LOAD, COMMISSION_AGENT, COMMISSION_MANDI 
	}; // IMPORTANT: the order of names here MUST be the same order as in the FEES_COMMISSION_FIELDS array defined in the AppSettings class. 

	/**
	 * Get a Map of JSON field names to SQLite Database field names.
	 * @return
	 */
	private Map<String, String> getColMap_Produce() {
		Map<String,String> map = new HashMap<String, String>();
		
		map.put(PRODUCE_WEB_ID, ProduceDataOpener.WEB_ID);
		map.put(PRODUCE_NAME_EN, ProduceDataOpener.PRODUCE_NAME_EN);
		map.put(PRODUCE_NAME_HI, ProduceDataOpener.PRODUCE_NAME_HI);
		
		return map;
	}
	
	/**
	 * Get a Map of JSON field names to SQLite Database field names.
	 * @return
	 */
	private Map<String, String> getColMap_License() {
		Map<String,String> map = new HashMap<String, String>();
		
		map.put(LICENSE_NAME, LicenseDataOpener.LICENSEE_NAME);
		map.put(LICENSE_ADDR_STREET, LicenseDataOpener.LICENSEE_ADDR_STREET);
		map.put(LICENSE_ADDR_STATE, LicenseDataOpener.LICENSEE_ADDR_STATE);
		map.put(LICENSE_ADDR_PINCODE, LicenseDataOpener.LICENSEE_ADDR_PIN);
		map.put(LICENSE_SELLER_NO, LicenseDataOpener.LICENSE_SELLER_LIC);
		map.put(LICENSE_BUYER_NO, LicenseDataOpener.LICENSE_BUYER_LIC);
		map.put(LICENSE_ORG, LicenseDataOpener.LICENSEE_ORG);
		
		return map;
	}
	
	/**
	 * Get a Map of JSON field names to SQLite Database field names.
	 * @return
	 */
	private Map<String, String> getColMap_Entry() {
		Map<String,String> map = new HashMap<String, String>();
		
		map.put(ENTRY_ID, EntryDataOpener.ENTRY_ID);
		map.put(ENTRY_TIMESTAMP, EntryDataOpener.TIMESTAMP);
		map.put(ENTRY_DRIVER_NAME, EntryDataOpener.DRIVER_NAME);
		map.put(ENTRY_VEHICLE_NO, EntryDataOpener.VEHICLE_NO);
		map.put(ENTRY_VEHICLE_TYPE, EntryDataOpener.VEHICLE_TYPE);
		map.put(ENTRY_PROD_ID, EntryDataOpener.PRODUCE_ID);
		map.put(ENTRY_PROD_WEIGHT, EntryDataOpener.PRODUCE_WEIGHT);
		map.put(ENTRY_PROD_FARM, EntryDataOpener.PRODUCE_FARM);
		map.put(ENTRY_BEARER_LIC, EntryDataOpener.BEARER_LIC);
		map.put(ENTRY_AGENT_LIC, EntryDataOpener.AGENT_LIC);
		
		return map;
	}
	
	/**
	 * Get a Map of JSON field names to AppSettings key names for fees only.
	 * @return
	 */
	private Map<String, String> getNameMap_Fees() {
		Map<String,String> map = new HashMap<String, String>();
		
		/* Fee names are important */
		if (FEES_COMMISSION_FIELDS.length != AppSettings.FEES_COMMISSION_FIELDS.length) {
			throw new RuntimeException("DbSynchronizer Error: Fees and Commissions fields defined in DbSynchronizer do not match those defined in AppSettings.  This is a mistake in the source code.");
		}
		
		int numFees = FEES_COMMISSION_FIELDS.length;
		for (int i = 0; i < numFees; i++) {
			map.put(FEES_COMMISSION_FIELDS[i], AppSettings.FEES_COMMISSION_FIELDS[i]);
		}
		
		return map;
	}
}
